liangerwen

基于 Next.js + ContentLayer + MDX 构建无后端博客系统

字数统计:3.5k|阅读时长:25分钟|发表于:2025-03-22

引言

在构建个人博客或技术文档时,无后端方案因其简单、高效和低成本而备受青睐。本文将介绍如何使用 Next.js、ContentLayer 和 MDX 构建一个无后端博客系统(本博客当前采用的方案),并探讨如何后续迁移到 MDX-Bundler 和 GitHub API,利用 GitHub Issues 作为文章存储源。

技术栈简介

  1. Next.js:React 框架,支持 SSR、SSG 和 API 路由,适合构建高性能博客。
  2. ContentLayer:将 Markdown 或 MDX 文件转换为类型安全的 JSON 数据,方便在 Next.js 中使用。
  3. MDX:支持在 Markdown 中嵌入 React 组件,增强文章的表现力。
  4. MDX-Bundler:将 MDX 文件编译为 React 组件,支持动态加载和自定义组件。
  5. GitHub API:通过 GitHub Issues 存储文章内容,实现无后端数据管理。

使用 Next.js + ContentLayer + MDX 构建博客系统

初始化项目

SH
  1. 1npx create-next-app@latest my-blog
  2. 2cd my-blog
  3. 3npm install contentlayer @mdx-js/loader

配置 ContentLayer

在项目根目录创建 contentlayer.config.ts 文件:

TS
contentlayer.config.ts
  1. 1import { defineDocumentType, makeSource } from "contentlayer/source-files";
  2. 2
  3. 3export const Post = defineDocumentType(() => ({
  4. 4 name: "Post",
  5. 5 filePathPattern: `**/*.mdx`,
  6. 6 fields: {
  7. 7 title: { type: "string", required: true },
  8. 8 date: { type: "date", required: true },
  9. 9 },
  10. 10 computedFields: {
  11. 11 slug: {
  12. 12 type: "string",
  13. 13 resolve: (post) => post._raw.flattenedPath,
  14. 14 },
  15. 15 },

创建文章

在 data/posts 目录下创建 Markdown 或 MDX 文件:

MDX
data/posts/hello.mdx
  1. 1---
  2. 2title: "Hello, Next.js!"
  3. 3date: 2023-10-01
  4. 4---
  5. 5
  6. 6This is a blog post written in **MDX**.

实现文章列表页

在 src/app/page.tsx 中实现文章列表:

TSX
src/app/page.tsx
  1. 1import { allPosts } from "contentlayer/generated";
  2. 2import Link from "next/link";
  3. 3
  4. 4export default function Home() {
  5. 5 return (
  6. 6 <div>
  7. 7 <h1>Blog Posts</h1>
  8. 8 <ul>
  9. 9 {allPosts.map((post) => (
  10. 10 <li key={post.slug}>
  11. 11 <Link href={`/posts/${post.slug}`}>{post.title}</Link>
  12. 12 </li>
  13. 13 ))}
  14. 14 </ul>
  15. 15 </div>

创建 mdx 渲染组件

在 src/components/mdx.tsx 中实现 mdx 渲染组件:

TSX
src/components/mdx.tsx
  1. 1import { useMDXComponent } from "next-contentlayer/hooks";
  2. 2
  3. 3export interface MdxProps {
  4. 4 code: string;
  5. 5}
  6. 6
  7. 7export default function Mdx({ code }: MdxProps) {
  8. 8 const Component = useMDXComponent(code);
  9. 9 return <Component />;
  10. 10}

实现文章详情页

在 src/app/posts/[slug].tsx 中实现文章详情页:

TSX
pages/posts/[...slug].tsx
  1. 1import Mdx from "@/src/components/mdx";
  2. 2import { allPosts } from "contentlayer/generated";
  3. 3
  4. 4interface IProps {
  5. 5 params: { slug: string[] };
  6. 6}
  7. 7
  8. 8export default function Post({ params }: IProps) {
  9. 9 const slug = decodeURIComponent(params.slug.join("/"));
  10. 10 const postIdx = allPosts.findIndex((p) => p.slug === slug);
  11. 11 const post = allPosts[postIdx];
  12. 12 if (!post) return notFound();
  13. 13
  14. 14 return (
  15. 15 <div>

迁移到 MDX-Bundler + GitHub API

安装 MDX-Bundler

SH
  1. 1npm install mdx-bundler

根据 repo 和 name 获取 issues

TS
src/utils/github.ts
  1. 1const REPO = "your repo";
  2. 2const NAME = "your name";
  3. 3
  4. 4export const fetchGithubIssueList = (type: "page" | "post", current: number) =>
  5. 5 fetch(
  6. 6 `https://api.github.com/search/issues?q=repo:${REPO}+state:open+author:${NAME}+${encodeURIComponent(
  7. 7 `[${type}]`
  8. 8 )}+in:title&per_page=10&sort=updated&page=${current}`
  9. 9 ).then((res) => res.json());
  10. 10
  11. 11export const fetchGithubIssueDetail = (id: number) =>
  12. 12 fetch(`https://api.github.com/repos/${REPO}/issues/${id}`).then((res) =>
  13. 13 res.json()
  14. 14 );

使用 MDX-Bundler 编译文章

TS
src/utils/parse-mdx.ts
  1. 1import { bundleMDX } from "mdx-bundler";
  2. 2
  3. 3export const parseMDX = (content: string) =>
  4. 4 bundleMDX({
  5. 5 source: content,
  6. 6 esbuildOptions: (opts) => {
  7. 7 opts.target = "es2020";
  8. 8 return opts;
  9. 9 },
  10. 10 });

改写文章列表页面

TSX
src/app/page.tsx
  1. 1import { fetchGithubIssueList } from "@/src/utils/github";
  2. 2import Link from "next/link";
  3. 3
  4. 4export default async function Home() {
  5. 5 const allPosts = await fetchGithubIssueList("post", 1);
  6. 6 return (
  7. 7 <div>
  8. 8 <h1>Blog Posts</h1>
  9. 9 <ul>
  10. 10 {allPosts.items.map((post) => (
  11. 11 <li key={post.id}>
  12. 12 <Link href={`/posts/${post.number}`}>{post.title}</Link>
  13. 13 </li>
  14. 14 ))}
  15. 15 </ul>

改写文章详情页面

TSX
pages/posts/[slug].tsx
  1. 1import { parseMDX } from "@/src/utils/parse-mdx";
  2. 2import { fetchGithubIssueDetail } from "@/src/utils/github";
  3. 3
  4. 4interface IProps {
  5. 5 params: { slug: string };
  6. 6}
  7. 7
  8. 8export default async function Post({ params }: IProps) {
  9. 9 const post = await fetchGithubIssueDetail(params.slug);
  10. 10 if (post.status === "404") return notFound();
  11. 11 const { code } = await parseMDX(post.body);
  12. 12 return (
  13. 13 <div>
  14. 14 <h1>{post.title}</h1>
  15. 15 <div>

总结

通过 Next.js + ContentLayer + MDX,我们可以快速构建一个无后端博客系统。而迁移到 MDX-Bundler + GitHub API 后,我们可以利用 GitHub Issues 作为文章存储源,实现更灵活的内容管理。这种方案不仅简单高效,还能充分利用现有的工具和平台,适合个人开发者和小型团队。


liangerwen

liangerwen

这瓜娃子懒得很,什么都没有留哈!